// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package org.apache.doris.common.util;

import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.cloud.security.SecurityChecker;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.thrift.TFileCompressType;
import org.apache.doris.thrift.TFileFormatType;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;

import java.io.BufferedReader;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.LongUnaryOperator;
import java.util.function.Predicate;

public class Util {
    private static final Logger LOG = LogManager.getLogger(Util.class);
    private static final Map<PrimitiveType, String> TYPE_STRING_MAP = new HashMap<PrimitiveType, String>();

    private static final long DEFAULT_EXEC_CMD_TIMEOUT_MS = 600000L;

    private static final String[] ORDINAL_SUFFIX
            = new String[] { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" };

    private static final List<String> REGEX_ESCAPES
            = Lists.newArrayList("\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|");

    static {
        TYPE_STRING_MAP.put(PrimitiveType.TINYINT, "tinyint(4)");
        TYPE_STRING_MAP.put(PrimitiveType.SMALLINT, "smallint(6)");
        TYPE_STRING_MAP.put(PrimitiveType.INT, "int(11)");
        TYPE_STRING_MAP.put(PrimitiveType.BIGINT, "bigint(20)");
        TYPE_STRING_MAP.put(PrimitiveType.LARGEINT, "largeint(40)");
        TYPE_STRING_MAP.put(PrimitiveType.FLOAT, "float");
        TYPE_STRING_MAP.put(PrimitiveType.DOUBLE, "double");
        TYPE_STRING_MAP.put(PrimitiveType.DATE, "date");
        TYPE_STRING_MAP.put(PrimitiveType.DATETIME, "datetime");
        TYPE_STRING_MAP.put(PrimitiveType.DATEV2, "datev2");
        TYPE_STRING_MAP.put(PrimitiveType.DATETIMEV2, "datetimev2");
        TYPE_STRING_MAP.put(PrimitiveType.CHAR, "char(%d)");
        TYPE_STRING_MAP.put(PrimitiveType.VARCHAR, "varchar(%d)");
        TYPE_STRING_MAP.put(PrimitiveType.JSONB, "json");
        TYPE_STRING_MAP.put(PrimitiveType.STRING, "string");
        TYPE_STRING_MAP.put(PrimitiveType.DECIMALV2, "decimal(%d, %d)");
        TYPE_STRING_MAP.put(PrimitiveType.DECIMAL32, "decimal(%d, %d)");
        TYPE_STRING_MAP.put(PrimitiveType.DECIMAL64, "decimal(%d, %d)");
        TYPE_STRING_MAP.put(PrimitiveType.DECIMAL128, "decimal(%d, %d)");
        TYPE_STRING_MAP.put(PrimitiveType.DECIMAL256, "decimal(%d, %d)");
        TYPE_STRING_MAP.put(PrimitiveType.HLL, "varchar(%d)");
        TYPE_STRING_MAP.put(PrimitiveType.BOOLEAN, "bool");
        TYPE_STRING_MAP.put(PrimitiveType.BITMAP, "bitmap");
        TYPE_STRING_MAP.put(PrimitiveType.QUANTILE_STATE, "quantile_state");
        TYPE_STRING_MAP.put(PrimitiveType.AGG_STATE, "agg_state");
        TYPE_STRING_MAP.put(PrimitiveType.ARRAY, "array<%s>");
        TYPE_STRING_MAP.put(PrimitiveType.VARIANT, "variant");
        TYPE_STRING_MAP.put(PrimitiveType.NULL_TYPE, "null");
    }

    public static LongUnaryOperator overflowSafeIncrement() {
        return original -> {
            if (original == Long.MAX_VALUE) {
                return Long.MAX_VALUE;
            }
            long r = original + 1;
            if (r == Long.MAX_VALUE || ((original ^ r) & (1 ^ r)) < 0) {
                // unbounded reached
                return Long.MAX_VALUE;
            } else {
                return r;
            }
        };
    }

    private static class CmdWorker extends Thread {
        private final Process process;
        private Integer exitValue;

        private StringBuffer outBuffer;
        private StringBuffer errBuffer;

        public CmdWorker(final Process process) {
            this.process = process;
            this.outBuffer = new StringBuffer();
            this.errBuffer = new StringBuffer();
        }

        public Integer getExitValue() {
            return exitValue;
        }

        public String getStdOut() {
            return this.outBuffer.toString();
        }

        public String getErrOut() {
            return this.errBuffer.toString();
        }

        @Override
        public void run() {
            BufferedReader outReader = null;
            BufferedReader errReader = null;
            String line = null;
            try {
                outReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                while ((line = outReader.readLine()) != null) {
                    outBuffer.append(line + '\n');
                }

                errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                while ((line = errReader.readLine()) != null) {
                    errBuffer.append(line + '\n');
                }

                exitValue = process.waitFor();
            } catch (InterruptedException e) {
                LOG.warn("get exception", e);
            } catch (IOException e) {
                LOG.warn("get exception", e);
            } finally {
                try {
                    if (outReader != null) {
                        outReader.close();
                    }
                    if (errReader != null) {
                        errReader.close();
                    }
                } catch (IOException e) {
                    LOG.warn("close buffered reader error", e);
                }
            }
        }
    }

    public static CommandResult executeCommand(String cmd, String[] envp) {
        return executeCommand(cmd, envp, DEFAULT_EXEC_CMD_TIMEOUT_MS);
    }

    public static CommandResult executeCommand(String cmd, String[] envp, long timeoutMs) {
        CommandResult result = new CommandResult();
        List<String> cmdList = shellSplit(cmd);
        String[] cmds = cmdList.toArray(new String[0]);

        try {
            Process p = Runtime.getRuntime().exec(cmds, envp);
            CmdWorker cmdWorker = new CmdWorker(p);
            cmdWorker.start();

            Integer exitValue = -1;
            try {
                cmdWorker.join(timeoutMs);
                exitValue = cmdWorker.getExitValue();
                if (exitValue == null) {
                    // if we get this far then we never got an exit value from the worker thread
                    // as a result of a timeout
                    LOG.warn("exec command [{}] timed out.", cmd);
                    exitValue = -1;
                }
            } catch (InterruptedException ex) {
                cmdWorker.interrupt();
                Thread.currentThread().interrupt();
                throw ex;
            } finally {
                p.destroy();
            }

            result.setReturnCode(exitValue);
            result.setStdout(cmdWorker.getStdOut());
            result.setStderr(cmdWorker.getErrOut());
        } catch (IOException e) {
            LOG.warn("execute command error", e);
        } catch (InterruptedException e) {
            LOG.warn("execute command error", e);
        }

        return result;
    }

    public static List<String> shellSplit(CharSequence string) {
        List<String> tokens = new ArrayList<String>();
        boolean escaping = false;
        char quoteChar = ' ';
        boolean quoting = false;
        StringBuilder current = new StringBuilder();
        for (int i = 0; i < string.length(); i++) {
            char c = string.charAt(i);
            if (escaping) {
                current.append(c);
                escaping = false;
            } else if (c == '\\' && !(quoting && quoteChar == '\'')) {
                escaping = true;
            } else if (quoting && c == quoteChar) {
                quoting = false;
            } else if (!quoting && (c == '\'' || c == '"')) {
                quoting = true;
                quoteChar = c;
            } else if (!quoting && Character.isWhitespace(c)) {
                if (current.length() > 0) {
                    tokens.add(current.toString());
                    current = new StringBuilder();
                }
            } else {
                current.append(c);
            }
        }
        if (current.length() > 0) {
            tokens.add(current.toString());
        }
        return tokens;
    }

    // Get a string represent the schema signature, contains:
    // list of columns and bloom filter column info.
    public static String getSchemaSignatureString(List<Column> columns) {
        StringBuilder sb = new StringBuilder();
        for (Column column : columns) {
            sb.append(column.getSignatureString(TYPE_STRING_MAP));
        }
        return sb.toString();
    }

    public static int generateSchemaHash() {
        return Math.abs(new SecureRandom().nextInt());
    }

    /**
     * Chooses k unique random elements from a population sequence
     */
    public static <T> List<T> sample(List<T> population, int kNum) {
        if (population.isEmpty() || population.size() < kNum) {
            return null;
        }

        Collections.shuffle(population);
        return population.subList(0, kNum);
    }

    /**
     * Delete directory and all contents in this directory
     */
    public static boolean deleteDirectory(File directory) {
        if (!directory.exists()) {
            return true;
        }

        if (directory.isDirectory()) {
            File[] files = directory.listFiles();
            if (null != files) {
                for (File file : files) {
                    if (file.isDirectory()) {
                        deleteDirectory(file);
                    } else {
                        file.delete();
                    }
                }
            }
        }
        return directory.delete();
    }

    public static String dumpThread(Thread t, int lineNum) {
        StringBuilder sb = new StringBuilder();
        StackTraceElement[] elements = t.getStackTrace();
        sb.append("dump thread: ").append(t.getName()).append(", id: ").append(t.getId()).append("\n");
        int count = lineNum;
        for (StackTraceElement element : elements) {
            if (count == 0) {
                break;
            }
            sb.append("    ").append(element.toString()).append("\n");
            --count;
        }
        return sb.toString();
    }

    // get response body as a string from the given url.
    // "encodedAuthInfo", the base64 encoded auth info. like:
    //      Base64.encodeBase64String("user:passwd".getBytes());
    // If no auth info, pass a null.
    public static String getResultForUrl(String urlStr, String encodedAuthInfo, int connectTimeoutMs,
            int readTimeoutMs) {
        StringBuilder sb = new StringBuilder();
        InputStream stream = null;
        try {
            SecurityChecker.getInstance().startSSRFChecking(urlStr);
            URL url = new URL(urlStr);
            URLConnection conn = url.openConnection();
            if (encodedAuthInfo != null) {
                conn.setRequestProperty("Authorization", "Basic " + encodedAuthInfo);
            }
            conn.setConnectTimeout(connectTimeoutMs);
            conn.setReadTimeout(readTimeoutMs);

            stream = (InputStream) conn.getContent();
            BufferedReader br = new BufferedReader(new InputStreamReader(stream));

            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
        } catch (Exception e) {
            LOG.warn("failed to get result from url: {}. {}", urlStr, e.getMessage());
            return null;
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    LOG.warn("failed to close stream when get result from url: {}", urlStr, e);
                    return null;
                }
            }
            SecurityChecker.getInstance().stopSSRFChecking();
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("get result from url {}: {}", urlStr, sb.toString());
        }
        return sb.toString();
    }

    public static long getLongPropertyOrDefault(String valStr, long defaultVal, Predicate<Long> pred,
            String hintMsg) throws AnalysisException {
        if (Strings.isNullOrEmpty(valStr)) {
            return defaultVal;
        }

        long result = defaultVal;
        try {
            result = Long.valueOf(valStr);
        } catch (NumberFormatException e) {
            throw new AnalysisException(hintMsg);
        }

        if (pred == null) {
            return result;
        }

        if (!pred.test(result)) {
            throw new AnalysisException(hintMsg);
        }

        return result;
    }

    public static double getDoublePropertyOrDefault(String valStr, double defaultVal, Predicate<Double> pred,
                                                String hintMsg) throws AnalysisException {
        if (Strings.isNullOrEmpty(valStr)) {
            return defaultVal;
        }

        double result = defaultVal;
        try {
            result = Double.parseDouble(valStr);
        } catch (NumberFormatException e) {
            throw new AnalysisException(hintMsg);
        }

        if (pred == null) {
            return result;
        }

        if (!pred.test(result)) {
            throw new AnalysisException(hintMsg);
        }

        return result;
    }

    public static boolean getBooleanPropertyOrDefault(String valStr, boolean defaultVal, String hintMsg)
            throws AnalysisException {
        if (Strings.isNullOrEmpty(valStr)) {
            return defaultVal;
        }

        try {
            return Boolean.valueOf(valStr);
        } catch (NumberFormatException e) {
            throw new AnalysisException(hintMsg);
        }
    }

    // not support encode negative value now
    public static void encodeVarint64(long source, DataOutput out) throws IOException {
        assert source >= 0;
        short B = 128; // CHECKSTYLE IGNORE THIS LINE

        while (source > B) {
            out.write((int) (source & (B - 1) | B));
            source = source >> 7;
        }
        out.write((int) (source & (B - 1)));
    }

    // not support decode negative value now
    public static long decodeVarint64(DataInput in) throws IOException {
        long result = 0;
        int shift = 0;
        short B = 128; // CHECKSTYLE IGNORE THIS LINE

        while (true) {
            int oneByte = in.readUnsignedByte();
            boolean isEnd = (oneByte & B) == 0;
            result = result | ((long) (oneByte & B - 1) << (shift * 7));
            if (isEnd) {
                break;
            }
            shift++;
        }

        return result;
    }

    // return the ordinal string of an Integer
    public static String ordinal(int i) {
        switch (i % 100) {
            case 11:
            case 12:
            case 13:
                return i + "th";
            default:
                return i + ORDINAL_SUFFIX[i % 10];
        }
    }

    // get an input stream from url, the caller is responsible for closing the stream
    // "encodedAuthInfo", the base64 encoded auth info. like:
    //      Base64.encodeBase64String("user:passwd".getBytes());
    // If no auth info, pass a null.
    public static InputStream getInputStreamFromUrl(String urlStr, String encodedAuthInfo, int connectTimeoutMs,
            int readTimeoutMs) throws IOException {
        boolean needSecurityCheck = !(urlStr.startsWith("/") || urlStr.startsWith("file://"));
        try {
            if (needSecurityCheck) {
                SecurityChecker.getInstance().startSSRFChecking(urlStr);
            }
            URL url = new URL(urlStr);
            URLConnection conn = url.openConnection();
            if (encodedAuthInfo != null) {
                conn.setRequestProperty("Authorization", "Basic " + encodedAuthInfo);
            }
            conn.setConnectTimeout(connectTimeoutMs);
            conn.setReadTimeout(readTimeoutMs);
            return conn.getInputStream();
        } catch (Exception e) {
            throw new IOException(e);
        } finally {
            if (needSecurityCheck) {
                SecurityChecker.getInstance().stopSSRFChecking();
            }
        }
    }

    public static boolean showHiddenColumns() {
        return ConnectContext.get() != null && (
            ConnectContext.get().getSessionVariable().showHiddenColumns()
            || ConnectContext.get().getSessionVariable().skipStorageEngineMerge());
    }

    public static String escapeSingleRegex(String s) {
        Preconditions.checkArgument(s.length() == 1);
        if (REGEX_ESCAPES.contains(s)) {
            return "\\" + s;
        }
        return s;
    }

    /**
     * Check all rules of catalog.
     */
    public static void checkCatalogAllRules(String catalog) throws AnalysisException {
        if (Strings.isNullOrEmpty(catalog)) {
            throw new AnalysisException("Catalog name is empty.");
        }

        if (!catalog.equals(InternalCatalog.INTERNAL_CATALOG_NAME)) {
            FeNameFormat.checkCommonName("catalog", catalog);
        }
    }

    public static void prohibitExternalCatalog(String catalog, String msg) throws AnalysisException {
        if (!Strings.isNullOrEmpty(catalog) && !catalog.equals(InternalCatalog.INTERNAL_CATALOG_NAME)) {
            throw new AnalysisException(String.format("External catalog '%s' is not allowed in '%s'", catalog, msg));
        }
    }

    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();

    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars);
    }


    @NotNull
    public static TFileFormatType getFileFormatTypeFromPath(String path) {
        String lowerCasePath = path.toLowerCase();
        if (lowerCasePath.contains(".parquet") || lowerCasePath.contains(".parq")) {
            return TFileFormatType.FORMAT_PARQUET;
        } else if (lowerCasePath.contains(".orc")) {
            return TFileFormatType.FORMAT_ORC;
        } else if (lowerCasePath.contains(".json")) {
            return TFileFormatType.FORMAT_JSON;
        } else {
            return TFileFormatType.FORMAT_CSV_PLAIN;
        }
    }

    public static TFileFormatType getFileFormatTypeFromName(String formatName) {
        String lowerFileFormat = Objects.requireNonNull(formatName).toLowerCase();
        if (lowerFileFormat.equals(FileFormatConstants.FORMAT_PARQUET)) {
            return TFileFormatType.FORMAT_PARQUET;
        } else if (lowerFileFormat.equals(FileFormatConstants.FORMAT_ORC)) {
            return TFileFormatType.FORMAT_ORC;
        } else if (lowerFileFormat.equals(FileFormatConstants.FORMAT_JSON)) {
            return TFileFormatType.FORMAT_JSON;
            // csv/csv_with_name/csv_with_names_and_types treat as csv format
        } else if (lowerFileFormat.equals(FileFormatConstants.FORMAT_CSV)
                || lowerFileFormat.equals(FileFormatConstants.FORMAT_CSV_WITH_NAMES)
                || lowerFileFormat.equals(FileFormatConstants.FORMAT_CSV_WITH_NAMES_AND_TYPES)) {
            return TFileFormatType.FORMAT_CSV_PLAIN;
        } else if (lowerFileFormat.equals(FileFormatConstants.FORMAT_HIVE_TEXT)) {
            return TFileFormatType.FORMAT_TEXT;
        } else if (lowerFileFormat.equals(FileFormatConstants.FORMAT_WAL)) {
            return TFileFormatType.FORMAT_WAL;
        } else if (lowerFileFormat.equals(FileFormatConstants.FORMAT_ARROW)) {
            return TFileFormatType.FORMAT_ARROW;
        } else {
            return TFileFormatType.FORMAT_UNKNOWN;
        }
    }

    /**
     * Infer {@link TFileCompressType} from file name.
     *
     * @param path of file to be inferred.
     */

    @NotNull
    public static TFileCompressType inferFileCompressTypeByPath(String path) {
        String lowerCasePath = path.toLowerCase();
        if (lowerCasePath.endsWith(".gz")) {
            return TFileCompressType.GZ;
        } else if (lowerCasePath.endsWith(".bz2")) {
            return TFileCompressType.BZ2;
        } else if (lowerCasePath.endsWith(".lz4")) {
            return TFileCompressType.LZ4FRAME;
        } else if (lowerCasePath.endsWith(".lzo")) {
            return TFileCompressType.LZOP;
        } else if (lowerCasePath.endsWith(".lzo_deflate")) {
            return TFileCompressType.LZO;
        } else if (lowerCasePath.endsWith(".deflate")) {
            return TFileCompressType.DEFLATE;
        } else if (lowerCasePath.endsWith(".snappy")) {
            return TFileCompressType.SNAPPYBLOCK;
        } else if (lowerCasePath.endsWith(".zst") || lowerCasePath.endsWith(".zstd")) {
            return TFileCompressType.ZSTD;
        } else {
            return TFileCompressType.PLAIN;
        }
    }

    public static TFileCompressType getFileCompressType(String compressType) throws AnalysisException {
        if (Strings.isNullOrEmpty(compressType)) {
            return TFileCompressType.UNKNOWN;
        }
        final String upperCaseType = compressType.toUpperCase();
        try {
            // for compatibility, convert lz4 to lz4frame
            if (upperCaseType.equals("LZ4")) {
                return TFileCompressType.LZ4FRAME;
            }
            return TFileCompressType.valueOf(upperCaseType);
        } catch (IllegalArgumentException e) {
            throw new AnalysisException("Unknown compression type: " + compressType);
        }
    }

    /**
     * Pass through the compressType if it is not {@link TFileCompressType#UNKNOWN}. Otherwise, return the
     * inferred type from path.
     */
    public static TFileCompressType getOrInferCompressType(TFileCompressType compressType, String path) {
        return compressType == TFileCompressType.UNKNOWN
                ? inferFileCompressTypeByPath(path.toLowerCase()) : compressType;
    }

    public static boolean isCsvFormat(TFileFormatType fileFormatType) {
        return fileFormatType == TFileFormatType.FORMAT_CSV_BZ2
                || fileFormatType == TFileFormatType.FORMAT_CSV_DEFLATE
                || fileFormatType == TFileFormatType.FORMAT_CSV_GZ
                || fileFormatType == TFileFormatType.FORMAT_CSV_LZ4FRAME
                || fileFormatType == TFileFormatType.FORMAT_CSV_LZ4BLOCK
                || fileFormatType == TFileFormatType.FORMAT_CSV_SNAPPYBLOCK
                || fileFormatType == TFileFormatType.FORMAT_CSV_LZO
                || fileFormatType == TFileFormatType.FORMAT_CSV_LZOP
                || fileFormatType == TFileFormatType.FORMAT_CSV_PLAIN;
    }

    public static void logAndThrowRuntimeException(Logger logger, String msg, Throwable e) {
        logger.warn(msg, e);
        throw new RuntimeException(msg, e);
    }

    public static String getRootCauseMessage(Throwable t) {
        String rootCause = "unknown";
        if (t == null) {
            return rootCause;
        }
        Throwable p = t;
        while (p.getCause() != null) {
            p = p.getCause();
        }
        String message = p.getMessage();
        if (message == null) {
            rootCause = p.getClass().getName();
        } else {
            rootCause = p.getClass().getName() + ": " + message;
        }
        return rootCause;
    }

    public static String getRootCauseWithSuppressedMessage(Throwable t) {
        String rootCause;
        Throwable p = t;
        while (p.getCause() != null) {
            p = p.getCause();
        }
        String message = p.getMessage();
        if (message == null) {
            rootCause = p.getClass().getName();
        } else {
            rootCause = p.getClass().getName() + ": " + p.getMessage();
        }
        StringBuilder msg = new StringBuilder(rootCause);
        Throwable[] suppressed = p.getSuppressed();
        for (int i = 0; i < suppressed.length; i++) {
            msg.append(" With suppressed").append("[").append(i).append("]:").append(suppressed[i].getMessage());
        }
        return msg.toString();
    }

    // Return the stack of the root cause
    public static String getRootCauseStack(Throwable t) {
        String rootStack = "unknown";
        if (t == null) {
            return rootStack;
        }
        Throwable p = t;
        while (p.getCause() != null) {
            p = p.getCause();
        }
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        p.printStackTrace(pw);
        return sw.toString();
    }

    public static Throwable getRootCause(Throwable t) {
        Throwable p = t;
        Throwable r = t;
        while (p != null) {
            r = p;
            p = p.getCause();
        }
        return r;
    }

    public static long sha256long(String str) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(str.getBytes(StandardCharsets.UTF_8));
            ByteBuffer buffer = ByteBuffer.wrap(hash);
            long result = buffer.getLong();
            // Handle Long.MIN_VALUE case to ensure non-negative ID generation
            if (result == Long.MIN_VALUE) {
                return str.hashCode();
            }
            return result;
        } catch (NoSuchAlgorithmException e) {
            return str.hashCode();
        }
    }

    // Only used for external db/table's id generation
    // And the db/table's id must >=0, see DescriptorTable.toThrift()
    public static long genIdByName(String... names) {
        return Math.abs(sha256long(String.join(".", names)));
    }

    public static String generateTempTableInnerName(String tableName) {
        if (tableName.indexOf(FeNameFormat.TEMPORARY_TABLE_SIGN) != -1) {
            return tableName;
        }

        ConnectContext ctx = ConnectContext.get();
        // when replay edit log, no need to generate temp table name
        return ctx == null ? tableName : ctx.getSessionId() + FeNameFormat.TEMPORARY_TABLE_SIGN + tableName;
    }

    public static String getTempTableDisplayName(String tableName) {
        return tableName.indexOf(FeNameFormat.TEMPORARY_TABLE_SIGN) != -1
            ? tableName.split(FeNameFormat.TEMPORARY_TABLE_SIGN)[1] : tableName;
    }

    public static String getTempTableSessionId(String tableName) {
        return tableName.indexOf(FeNameFormat.TEMPORARY_TABLE_SIGN) != -1
            ? tableName.split(FeNameFormat.TEMPORARY_TABLE_SIGN)[0] : "";
    }

    public static boolean isTempTable(String tableName) {
        return tableName.indexOf(FeNameFormat.TEMPORARY_TABLE_SIGN) != -1;
    }

    public static boolean isTempTableInCurrentSession(String tableName) {
        return getTempTableSessionId(tableName).equals(ConnectContext.get().getSessionId());
    }

    // randomly return the Long from given long arrays
    public static Long getRandomLong(long... numbers) {
        if (numbers == null || numbers.length == 0) {
            return null;
        }
        int index = (int) (Math.random() * numbers.length);
        return numbers[index];
    }

    // randomly return the Long from given long arrays
    public static Integer getRandomInt(int... numbers) {
        if (numbers == null || numbers.length == 0) {
            return null;
        }
        int index = (int) (Math.random() * numbers.length);
        return numbers[index];
    }

    // randomly return the String from given String arrays
    public static String getRandomString(String... strs) {
        if (strs == null || strs.length == 0) {
            return null;
        }
        int index = (int) (Math.random() * strs.length);
        return strs[index];
    }
}
